use rand::{rngs::ThreadRng, RngCore};
use rcgen::{
    Certificate, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, KeyPair,
    SerialNumber,
};
use rocket::time::OffsetDateTime;
use rocket_db_pools::sqlx::{self, sqlite::SqliteRow, Row, Sqlite, Transaction};
use std::{cmp::Ordering, time::Duration};

use crate::model::device::{Device, DeviceId, DeviceInfo, DeviceName};

pub fn new_serial_number() -> Result<SerialNumber, rand::Error> {
    let mut rand = ThreadRng::default();
    let mut buffer = [0; 16];
    rand.try_fill_bytes(&mut buffer)?;
    Ok(SerialNumber::from_slice(&buffer))
}

fn map_device_row(row: SqliteRow) -> Result<Device, sqlx::Error> {
    Ok(Device {
        id: DeviceId(row.try_get("id")?),
        info: DeviceInfo {
            name: DeviceName(row.try_get("name")?),
            certificate: row.try_get("certificate")?,
            enabled: row.try_get("enabled")?,
        },
    })
}

pub enum DeleteStatus {
    Deleted,
    NotFound,
}

pub enum UpdateEnabledError {
    Database(sqlx::Error),
    NotFound,
}

impl From<sqlx::Error> for UpdateEnabledError {
    fn from(value: sqlx::Error) -> Self {
        UpdateEnabledError::Database(value)
    }
}

impl DeviceId {
    #[allow(unused)]
    pub async fn get_device<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<Option<Device>, sqlx::Error> {
        sqlx::query("SELECT id, name, certificate, enabled FROM devices WHERE id = ?")
            .bind(self.0)
            .try_map(map_device_row)
            .fetch_optional(&mut **db)
            .await
    }

    pub async fn update_enabled<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        enabled: bool,
    ) -> Result<(), UpdateEnabledError> {
        let updated_rows = sqlx::query("UPDATE devices SET enabled = ? WHERE id = ?")
            .bind(enabled)
            .bind(self.0)
            .execute(&mut **db)
            .await?;
        match updated_rows.rows_affected().cmp(&1) {
            Ordering::Less => Err(UpdateEnabledError::NotFound),
            Ordering::Equal => Ok(()),
            Ordering::Greater => panic!("Updated more than one row"),
        }
    }

    pub async fn delete_device<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<DeleteStatus, sqlx::Error> {
        let deleted_rows = sqlx::query("DELETE FROM devices WHERE id = ?")
            .bind(self.0)
            .execute(&mut **db)
            .await?;

        match deleted_rows.rows_affected().cmp(&1) {
            Ordering::Less => Ok(DeleteStatus::NotFound),
            Ordering::Equal => Ok(DeleteStatus::Deleted),
            Ordering::Greater => panic!("Deleted more than one row"),
        }
    }
}

pub async fn get_devices<'a>(db: &mut Transaction<'a, Sqlite>) -> Result<Vec<Device>, sqlx::Error> {
    sqlx::query("SELECT id, name, certificate, enabled FROM devices")
        .try_map(map_device_row)
        .fetch_all(&mut **db)
        .await
}

pub enum CreateDeviceError {
    ExistingDeviceName,
    InvalidName,
    Database(sqlx::Error),
    CreateCertificate(rcgen::Error),
    CreateSerial(rand::Error),
}

impl From<sqlx::Error> for CreateDeviceError {
    fn from(value: sqlx::Error) -> Self {
        CreateDeviceError::Database(value)
    }
}

impl From<rcgen::Error> for CreateDeviceError {
    fn from(value: rcgen::Error) -> Self {
        CreateDeviceError::CreateCertificate(value)
    }
}

impl From<rand::Error> for CreateDeviceError {
    fn from(value: rand::Error) -> Self {
        CreateDeviceError::CreateSerial(value)
    }
}

pub async fn create_device<'a>(
    db: &mut Transaction<'a, Sqlite>,
    ca_certificate: &Certificate,
    ca_key_pair: &KeyPair,
    name: DeviceName,
) -> Result<(Device, KeyPair), CreateDeviceError> {
    if !name.is_valid() {
        return Err(CreateDeviceError::InvalidName);
    }

    let device_name_exists: bool =
        sqlx::query("SELECT EXISTS (SELECT 1 FROM devices WHERE name = ?)")
            .bind(&name.0)
            .map(|row: SqliteRow| row.get(0))
            .fetch_one(&mut **db)
            .await?;

    if device_name_exists {
        return Err(CreateDeviceError::ExistingDeviceName);
    }

    let device_keypair = KeyPair::generate()?;

    let mut certificate_params = CertificateParams::new(vec![name.0.clone()])?;

    let mut distinguished_name = DistinguishedName::new();
    distinguished_name.push(DnType::CommonName, name.0.as_str());
    certificate_params.distinguished_name = distinguished_name;

    certificate_params.serial_number = Some(new_serial_number()?);

    let now = OffsetDateTime::now_utc();
    certificate_params.not_before = now;
    certificate_params.not_after = now + Duration::from_secs(10 * 365 * 24 * 3600);

    certificate_params
        .extended_key_usages
        .push(ExtendedKeyUsagePurpose::ClientAuth);

    let certificate = certificate_params.signed_by(&device_keypair, ca_certificate, ca_key_pair)?;
    let certificate = certificate.pem();

    sqlx::query(
        r#"
            INSERT INTO devices
            (name, certificate, enabled)
            VALUES (?, ?, ?)
        "#,
    )
    .bind(&name.0)
    .bind(&certificate)
    .bind(true)
    .execute(&mut **db)
    .await?;

    let id: DeviceId = sqlx::query("SELECT LAST_INSERT_ROWID()")
        .map(|row: SqliteRow| DeviceId(row.get(0)))
        .fetch_one(&mut **db)
        .await?;

    let info: DeviceInfo = DeviceInfo {
        name,
        certificate,
        enabled: true,
    };

    Ok((Device { id, info }, device_keypair))
}
